const kSTREAM_INPUT = 1;
const kSTREAM_OUTPUT = 2;
const kSTREAM_TYPE_FILE = 1;
const kSTREAM_TYPE_FILE_BINARY = 2;
const kSTREAM_TYPE_STR = 3;
const kSTREAM_TYPE_CONSOLE = 10;
const kSEEK_SET = Ci.nsISeekableStream.NS_SEEK_SET;
const kSEEK_CUR = Ci.nsISeekableStream.NS_SEEK_CUR;
const kSEEK_END = Ci.nsISeekableStream.NS_SEEK_END;

let gStdout = function() {
  let s = new OutputStream(kSTREAM_TYPE_CONSOLE, "Application.console");
  s.costream = {
    writeString: function writeString(aString) {
      Application.console.log(aString);
    },
    flush: function flush() {
      //do nothing
    },
    close: function close() {
      //do nothing
    }
  };
  return s;
}();


function EofObject() {
  //empty.
}

function EofObjectP(aObj) {
  return (aObj.constructor == EofObject);
}

function Stream(aDirection) {
  this._dir = aDirection;
  this._closed = false;
}

Stream.prototype = {
  get dir() {
    return this._dir;
  },
  get type() {
    return this._type;
  },
  get name() {
    return this._name;
  },
  get closed() {
    return this._closed;
  }
};

function InputStream(aStreamType, aStreamName, aIstream, aByteStream, aCharStream, aLocalFile, aPump) {
  this._type     = aStreamType;
  this._name     = aStreamName;
  this._istream  = aIstream;
  this._bistream = aByteStream;
  this._cistream = aCharStream;
  this._file     = aLocalFile;
  this._pump     = aPump;
}

InputStream.prototype = new Stream(kSTREAM_INPUT);
extend(InputStream.prototype, {
  get istream() {
    return this._istream;
  },
  get bistream() {
    return this._bistream;
  },
  get cistream() {
    return this._cistream;
  },
  get pump() {
    return this._pump;
  },
  get file() {
    return this._file;
  }
});

function OutputStream(aStreamType, aStreamName, aOstream, aByteStream, aCharStream, aLocalFile, aStorage) {
  this._type     = aStreamType;
  this._name     = aStreamName;
  this._ostream  = aOstream;
  this._bostream = aByteStream;
  this._costream = aCharStream;
  this._file     = aLocalFile;
  this._storage  = aStorage;
}

OutputStream.prototype = new Stream(kSTREAM_OUTPUT);
extend(OutputStream.prototype, {
  get ostream() {
    return this._ostream;
  },
  get bostream() {
    return this._bostream;
  },
  get costream() {
    return this._costream;
  },
  get file() {
    return this._file;
  },
  get storage() {
    return this._storage;
  }
});

function streamP(aStream) {
  return (aStream instanceof Stream);
}

function inputStreamP(aInputStream) {
  return (aInputStream instanceof InputStream);
}

function outputStreamP(aOutputStream) {
  return (aOutputStream instanceof OutputStream);
}

function closedStreamP(aStream) {
  return (streamP(aStream) && aStream.closed);
}

//TODO We need a bunch of type checks for arguments.

function closeInputStream(aInputStream) {
  aInputStream._closed = true;
  if (aInputStream.cistream) aInputStream.cistream.close();
  if (aInputStream.bistream) aInputStream.bistream.close();
}

function closeOutputStream(aOutputStream) {
  aOutputStream._closed = true;
  // Watch out NS_BASE_STREAM_CLOSED error,
  // you need to close costream firstly then bostream.
  aOutputStream.costream.close();
  aOutputStream.bostream.close();
}

// TODO streamBuffering
// TODO streamCurrentLine
// TODO streamFileNumber

function streamSeek(aStream, aOffset, aWhence) {
  aWhence = typeof aWhence == "number" ? aWhence : Ci.nsISeekableStream.NS_SEEK_SET;
  if (inputStreamP(aStream) && aStream.istream.seek) {
    //force the character-input-stream to follow the seeked point.
    //I don't know why this hack works.
    //
    //     if (inputStreamp(aStream) && aStream.type == kSTREAM_TYPE_FILE) {
    //       let hasmore = false;
    //       do {
    //         hasmore = aStream.cistream.readString(8192, {});
    //       } while (hasmore);
    //       do {
    //         hasmore = aStream.cistream.readLine({});
    //       } while (hasmore);
    //     }
    //
    return aStream.istream.seek(aWhence, aOffset);
  }
  if (outputStreamP(aStream) && aStream.ostream.seek) {
    return aStream.ostream.seek(aWhence, aOffset);
  }
  throw new Error("not a nsISeekableStream" + aStream);
}

function streamTell(aStream) {
  if (inputStreamP(aStream) && aStream.istream.tell) {
    return aStream.istream.tell();
  }
  if (outputStreamP(aStream) && aStream.ostream.tell) {
    return aStream.ostream.tell();
  }
  throw new Error("not a nsISeekableStream" + aStream);
}

function copyStream(aInputStream, aOutputStream, aUnit) {
  //TODO handle aUnit.
  aOutputStream.ostream.writeFrom(aInputStream.istream,
                                  aInputStream.istream.available());
  aOutputStream.ostream.flush();
}

function charReadyP(aInputStream) {
  return (aInputStream.istream.available() > 0);
}

function byteReadyP(aInputStream) {
  return (aInputStream.istream.available() > 0);
}

function streamToString(aInputStream) {
  let str = {}, hasmore, strs = [];
  do {
    hasmore = aInputStream.cistream.readString(8192, str);
    if (str.value) strs.push(str.value);
  } while (hasmore)
  return strs.join("");
}

function streamToStringArray(aInputStream) {
  let line = {}, hasmore, lines = [];
  do {
    hasmore = aInputStream.cistream.readLine(line);
    lines.push(line.value);
  } while (hasmore);
  return lines;
}

function _internal0(aReader) {
  let v, arr = [];
  for(;;) {
    v = aReader();
    if (EofObjectP(v)) break;
    arr.push(v);
  }
  return arr;
}

function streamToArray(aReader, aInputStream) {
  return _internal0(function() {return aReader(aInputStream);});
}

function streamReduce(aProc, akNil, aReader) {
  return _internal0(aReader).reduce(aProc, akNil);
}

function streamReduceRight(aProc, akNil, aReader) {
  return _internal0(aReader).reduceRight(aProc, akNil);
}

function streamForEach(aProc, aReader) {
  return _internal0(aReader).forEach(aProc);
}

function streamMap(aProc, aReader) {
  return _internal0(aReader).map(aProc);
}

function openInputFile(aFilePath, aOpt) {
  //TODO handle aOpt;
  let ifDoesNotExist;
  // TODO handle aOpt.ifDoesNotExist "error" "f";
  if (aOpt) {
    if (aOpt.ifDoesNotExist == "error") {};
    if (aOpt.ifDoesNotExist == false) {}
  }

  let localFile = Cc["@mozilla.org/file/local;1"].
                    createInstance(Ci.nsILocalFile);
  let fistream = Cc["@mozilla.org/network/file-input-stream;1"].
                  createInstance(Ci.nsIFileInputStream);
  localFile.initWithPath(aFilePath);
  fistream.init(localFile, 0x01, 0, 0);

  let istream = Cc["@mozilla.org/network/buffered-input-stream;1"].
                  createInstance(Ci.nsIBufferedInputStream);
  istream.init(fistream, 8192);
  istream.QueryInterface(Ci.nsISeekableStream);

  let bistream = Cc["@mozilla.org/binaryinputstream;1"].
                   createInstance(Ci.nsIBinaryInputStream);
  bistream.setInputStream(istream);

  let cistream = Cc["@mozilla.org/intl/converter-input-stream;1"].
                   createInstance(Ci.nsIConverterInputStream);
  cistream.init(istream, "UTF-8", 0, Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
  cistream.QueryInterface(Ci.nsIUnicharLineInputStream);

  return new InputStream(kSTREAM_TYPE_FILE, aFilePath, istream, bistream, cistream, localFile);
}

function openOutputFile(aFilePath, aOpt) {
  let ifExists = 0x20; // PR_TRUNCATE
  let ifDoesNotExist = 0x08; //PR_CREATE_FILE
  if (aOpt) {
    if (aOpt.ifExists) {
      switch (aOpt.ifExists) {
      case "supersede":
        ifExists = 0x20;
        break;
      case "append":
        ifExists = 0x10;
        break;
      }
    }
    if (aOpt.ifDoesNotExist) {
      switch (aOpt.ifDoesNotExist) {
      case "create":
        ifDoesNotExist = 0x08;
        break;
      }
    }
    // TODO handle aOpt.ifExists "overwrite" "error" "f";
    if (aOpt.ifExists == "error" ||
        aOpt.ifExists == false)
      ifDoesNotExist |= 0x80;
  }

  let localFile = Cc["@mozilla.org/file/local;1"].
                    createInstance(Ci.nsILocalFile);
  let fostream = Cc["@mozilla.org/network/file-output-stream;1"].
                   createInstance(Ci.nsIFileOutputStream);
  localFile.initWithPath(aFilePath);
  fostream.init(localFile, 0x02 | ifExists | ifDoesNotExist, 0, 0);

  let ostream = Cc["@mozilla.org/network/buffered-output-stream;1"].
                  createInstance(Ci.nsIBufferedOutputStream);
  ostream.init(fostream, 8192);
  ostream.QueryInterface(Ci.nsISeekableStream);

  let bostream = Cc["@mozilla.org/binaryoutputstream;1"].
                   createInstance(Ci.nsIBinaryOutputStream);
  bostream.setOutputStream(fostream);

  let costream = Cc["@mozilla.org/intl/converter-output-stream;1"].
                   createInstance(Ci.nsIConverterOutputStream);
  costream.init(fostream, "UTF-8", 0, Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);

  return new OutputStream(kSTREAM_TYPE_FILE, aFilePath, ostream, bostream, costream, localFile);
}

function callWithInputFile(aFilePath, aProc, aOpt) {
  let s = openInputFile(aFilePath, aOpt);
  let res = aProc(s);
  closeInputStream(s);
  return res;
}

function callWithOutputFile(aFilePath, aProc, aOpt) {
  let s = openOutputFile(aFilePath, aOpt);
  let res = aProc(s);
  closeOutputStream(s);
  return res;
}

function openInputString(aString) {
  let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
                    createInstance(Ci.nsIScriptableUnicodeConverter);
  converter.charset = "UTF-8";
  let istream = Cc["@mozilla.org/network/buffered-input-stream;1"].
                  createInstance(Ci.nsIBufferedInputStream);
  istream.init(converter.convertToInputStream(aString), 8192);
  istream.QueryInterface(Ci.nsISeekableStream);

  let bistream = Cc["@mozilla.org/binaryinputstream;1"].
                   createInstance(Ci.nsIBinaryInputStream);
  bistream.setInputStream(istream);

  let cistream = Cc["@mozilla.org/intl/converter-input-stream;1"].
                    createInstance(Ci.nsIConverterInputStream);
  cistream.init(istream, "UTF-8", 0, Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
  cistream.QueryInterface(Ci.nsIUnicharLineInputStream);

  return new InputStream(kSTREAM_TYPE_STR, "input string stream", istream, bistream, cistream);
}

function getRemainingInputString(aInputStringStream) {
  // not implemented yet.
  // We can't keep a seek point of the character-input-stream's internal buffer.
}

function _internal1(aStorage) {
  let ostream = Cc["@mozilla.org/network/buffered-output-stream;1"].
                  createInstance(Ci.nsIBufferedOutputStream);
  ostream.init(aStorage.getOutputStream(0), 8192);
  ostream.QueryInterface(Ci.nsISeekableStream);

  let bostream = Cc["@mozilla.org/binaryoutputstream;1"].
                   createInstance(Ci.nsIBinaryOutputStream);
  bostream.setOutputStream(ostream);

  let costream = Cc["@mozilla.org/intl/converter-output-stream;1"].
                   createInstance(Ci.nsIConverterOutputStream);
  costream.init(ostream, "UTF-8", 8192, 0x0000);

  return [ostream, bostream, costream];
}

function openOutputString() {
  let storage = Cc["@mozilla.org/storagestream;1"].
                  createInstance(Ci.nsIStorageStream);
  // I have no idea what to pick for the first parameter (segments)
  storage.init(4, 0xffffffff, null);
  let [ostream, bostream, costream] = _internal1(storage);

  return new OutputStream(kSTREAM_TYPE_STR, "output string stream", ostream, bostream, costream, null, storage);
}

function getOutputString(aOutputStream) {
  closeOutputStream(aOutputStream);
  let istream = aOutputStream.storage.newInputStream(0);

  let cistream = Cc["@mozilla.org/intl/converter-input-stream;1"].
                   createInstance(Ci.nsIConverterInputStream);
  cistream.init(istream, "UTF-8", 0, Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);

  let str = {}, hasmore, strs = [];
  do {
    hasmore = cistream.readString(8192, str);
    if (str.value) strs.push(str.value);
  } while (hasmore)
  cistream.close();

  [aOutputStream._ostream, aOutputStream._bostream, aOutputStream._costream]
    = _internal1(aOutputStream.storage);
  return strs.join("");
}

function callWithInputString(aString, aProc) {
  let ins = openInputString(aString);
  let r = aProc(ins);
  closeInputStream(ins);
  return r;
}

function callWithOutputString(aProc) {
  let outs = openOutputString();
  aProc(outs);
  let r = getOutputString(outs);
  closeOutputStream(outs);
  return r;
}

function callWithStringIO(aString, aProc) {
  let ins  = openInputString(aString);
  let outs = openOutputString();
  aProc(ins, outs);
  let r = getOutputString(outs);
  closeInputStream(ins);
  closeOutputStream(outs);
  return r;
}

function read(aInputStream) {
  return NativeJSON.decodeFromStream(aInputStream.istream,
                                     aInputStream.istream.available());
}

function readChar(aInputStream) {
  let str = {};
  let hasmore = aInputStream.cistream.readString(1, str);
  if (hasmore) return str.value;
  return new EofObject();
}

function peekChar(aInputStream) {
  // impossible to implement?
}

function readByte(aInputStream) {
  if (byteReadyp(aInputStream)) {
    let b = aInputStream.bistream.readByteArray(1);
    if (b) return b[0];
  }
  return new EofObject();
}

function peekByte(aInputStream) {
  // kSTREAM_TYPE_FILE(BINARY) has seek. kSTREAM_TYPE_STR doesn't.
  if (aInputStream.istream.seek) {
    let r = readByte(aInputStream);
    if (!EofObjectp(r)) streamSeek(aInputStream, kSEEK_CUR, -1);
    return r;
  }
  throw new Error("not a nsISeekableStream" + aInputStream);
}

function readLine(aInputStream, aAllowByteStringp) {
  let line = {};
  aInputStream.cistream.readLine(line);
  if (line.value) return line.value;
  return new EofObject();
}

function readBlock(aNBytes, aInputStream) {
  let block = aInputStream.bistream.readBytes(aNBytes);
  if (block && block.value) return block.value;
  return new EofObject();
}

function write(aObj, aOutputStream) {
  if (typeof aObj == "Object" && aOutputStream.type != kSTREAM_TYPE_CONSOLE) {
    NativeJSON.encodeToStream(aOutputStream.ostream, "UTF-8", false, aObj);
  } else {
    aOutputStream.costream.writeString(aObj.toString ? aObj.toString() : aObj);
  }
}

function display(aObj, aOutputStream) {
  aOutputStream.costream.writeString(aObj.toString ? aObj.toString() : aObj);
}

function newline(aOutputStream) {
  aOutputStream.costream.writeString("/n");
}

function flush(aOutputStream) {
  aOutputStream. ostream.flush();
  aOutputStream.bostream.flush();
  aOutputStream.costream.flush();
}

function writeChar(aChar, aOutputStream) {
  aOutputStream.costream.writeString(aChar);
}

function writeByte(aByte, aOutputStream) {
  if (aOutputStream.bostream.write8) {
    aOutputStream.bostream.write8(aByte);
  } else {
    throw new Error("not a output binary stream" + aOutputStream);
  }
}

function writeBlock(aBytes, aOutputStream) {
  if (aOutputStream.bostream.writeBytes) {
    aOutputStream.bostream.writeByteArray(aBytes, aBytes.length);
  } else {
    throw new Error("not a output binary stream" + aOutputStream);
  }
}

function writeToString(aObj, aWriter) {
  aWriter = aWriter ? aWriter : write;
  return callWithOutputString(function(outs) {aWriter(outs, aObj);});
}

function readFromString(aString, aStart, aEnd) {
  aStart = aStart || 0;
  aEnd   = aEnd   || aString.length;
  return callWithInputString(aString.substring(aStart, aEnd), read);
}

function format(aString) {
  let len = s.length;
  let s = openOutputString();
  for (let i = 0, j = 1; i < len;) {
    let i2 = aString.indexOf("~", i);
    if (i2 == -1) {
      write(aString.substring(i,len), s);
	    return getOutputString(s);
    }
    if (i2 == (len - 1)) {
	    write(aString.substring(i, len), s);
	    return getOutputString(s);
    }
    if (i2 > i) write(aString.substring(i, i2), s);
	  // already advance before evalualating escape sequences.
	  // this way it is easier to see.
	  // no escape sequence requires 'i'.
	  i = i2 + 2;
	  switch(s[i2 + 1]) {
	    case "A": //fall through
	    case "a":
		    display(arguments[j], s);
	      j++;
	      break;
	    case "S":
	    case "s":
		    write(arguments[j], s);
	      j++;
	      break;
	    case "V":
	    case "v":
		    display(arguments[j], s);
        newline(s);
	      j++;
	      break;
	    case "C":
	    case "c":
	      write(String.fromCharCode(arguments[j]), s);
	      j++;
	      break;
	    case "X":
	    case "x":
        write(arguments[j].toString(16), s);
	      j++;
	      break;
	    case "O":
	    case "o":
	      write(arguments[j].toString(8), s);
	      j++;
	      break;
	    case "B":
	    case "b":
	      write(arguments[j].toString(2), s);
	      j++;
	      break;
	    case "%":
	    case "n":
	      newline(s);
	      break;
	    case "r":
	      write("\r", s);
	      break;
	    case "~":
	      write("~", s);
	      break;
	    default:
	      throw new Error("format: illegal ~" + s[i2 + 1] + " sequence");
	  }
  }
  return getOutputString(s);
}
